package mc.support.crawler;

import org.apache.http.*;
import org.apache.http.client.methods.HttpGet;
import org.apache.http.client.params.ClientPNames;
import org.apache.http.client.params.CookiePolicy;
import org.apache.http.conn.ConnectTimeoutException;
import org.apache.http.conn.scheme.PlainSocketFactory;
import org.apache.http.conn.scheme.Scheme;
import org.apache.http.conn.scheme.SchemeRegistry;
import org.apache.http.conn.scheme.SchemeSocketFactory;
import org.apache.http.conn.ssl.SSLSocketFactory;
import org.apache.http.impl.client.DefaultHttpClient;
import org.apache.http.impl.conn.PoolingClientConnectionManager;
import org.apache.http.params.*;
import org.apache.http.util.EntityUtils;

import java.io.IOException;
import java.net.*;
import java.util.*;

/**
 * Created with IntelliJ IDEA. <br/>
 * Author: Francis Yun    <br/>
 * Date: 2014-04-23  <br/>
 */
public class SimpleHttpClient {
    public static String[] USER_AGENTS = {
            "Mozilla/5.0 (Windows NT 6.1; WOW64; rv:20.0) Gecko/20100101 Firefox/20.0",
            "Mozilla/5.0 (compatible; MSIE 9.0; qdesk 2.4.1263.203; Windows NT 6.1; WOW64; Trident/6.0)",
            "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_8_5) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/30.0.1599.101 Safari/537.36",
            "Mozilla/5.0 (Macintosh; Intel Mac OS X 10.8; rv:24.0) Gecko/20100101 Firefox/24.0",
            "User-Agent Mozilla/5.0 (Macintosh; Intel Mac OS X 10_8_5) AppleWebKit/537.71 (KHTML, like Gecko) Version/6.1 Safari/537.71",
            "Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.17 (KHTML, like Gecko) Chrome/24.0.1312.69 Safari/537.17"
    };

    private static final Map<String, String> DEFAULT_HEAD_MAP = new HashMap<String, String>() {
        private static final long serialVersionUID = -5683614764357811276L;
        {
            put( "Pragma", "no-cache" );
            put( "Cache-Control", "max-age=0, no-cache, no-store, must-revalidate" );
            put( "Connection","keep-alive" );
            put( "Accept","text/html,application/xhtml+xml,application/xml,application/x-javascript;q=0.9,*/*;q=0.8" );
            put( "Accept-Language","zh-cn,zh;q=0.8,en-us;q=0.5,en;q=0.3" );
            put( "Accept-Charset", "utf-8, gbk;q=0.7,*;q=0.7" );
        }

    };

    public static final String DEFAULT_CHARSET = "utf-8";

    public static final int DEFAULT_RETRY_TIMES = 3;

    private String charset;

    private int retryTimes;

    private List<ProxyConfig> proxies;
    private boolean proxyEnable = false;
    private Random random;

    public SimpleHttpClient() {
        this( DEFAULT_CHARSET, DEFAULT_RETRY_TIMES );
    }

    public SimpleHttpClient(Map<String, Integer> proxyList) {
        this( DEFAULT_CHARSET, DEFAULT_RETRY_TIMES );
        if ( null != proxyList && !proxyList.isEmpty() ) {

            proxies = new ArrayList<>( proxyList.size() );
            for ( Map.Entry<String, Integer> e : proxyList.entrySet() ) {
                String addr = e.getKey();
                Integer port = e.getValue();
                if ( null != addr && null != port ) {
                    proxies.add( new ProxyConfig( addr, port ) );
                }
            }
            proxyEnable = true;
            random = new Random();
        }
    }

    public SimpleHttpClient(String charset, int retryTimes) {
        this.charset = charset;
        this.retryTimes = retryTimes;
    }

    String getCharset() {
        return charset;
    }

    ProxyConfig getProxyConfig() {
        int len = proxies.size();
        return proxies.get( random.nextInt( len ) );
    }

    void setCharset(String charset) {
        this.charset = charset;
    }

    public String doQuery(String url) throws IOException {
        return doQuery( url, 0 );
    }

    private String doQuery(String url, int queryTime) throws IOException {

        HttpGet get = new HttpGet( url );
        get = assemblyHeaders( get );

        String html = null;
        DefaultHttpClient client = getHttpClient();
        try {
            HttpResponse resp = client.execute( get );
            html = parseResp( url, resp, queryTime );
        } finally {
            client.getCookieStore().clear();
        }

        return html;
    }

    private String parseResp(String originalURL, HttpResponse resp, int queryTime) throws IOException {
        int status = resp.getStatusLine().getStatusCode();
        String html = "";

        if ( queryTime >= retryTimes ) {
            return html;
        }

        switch ( status ) {
            case HttpStatus.SC_OK: html = handleSuccessResp( resp ); break;
            case HttpStatus.SC_MOVED_PERMANENTLY:
            case HttpStatus.SC_MOVED_TEMPORARILY: html = handleMovedResp( resp ); break;
            default: html = doQuery( originalURL, ++queryTime );
        }

        return html;
    }

    private String handleSuccessResp( HttpResponse resp ) throws IOException {
        HttpEntity entity = resp.getEntity();
        String html = "";
        if ( null != entity ) {
            html = EntityUtils.toString(entity, charset);
            EntityUtils.consume(entity);
        }
        return html;
    }

    private String handleMovedResp( HttpResponse resp ) throws IOException {
        Header locationHeader = resp.getFirstHeader("Location");
        String location;
        String html = "";
        if (locationHeader != null) {
            location = locationHeader.getValue();
            html = doQuery( location, 0 );
        }

        return html;
    }

    private HttpGet assemblyHeaders(HttpGet get) {
        for ( Map.Entry<String, String> e: DEFAULT_HEAD_MAP.entrySet() ) {
            get.setHeader( e.getKey(), e.getValue() );
        }

        Random r = new Random();
        String userAgent = USER_AGENTS[ r.nextInt( USER_AGENTS.length ) ];
        get.setHeader( "User-Agent", userAgent );

        return get;
    }

    private SchemeRegistry schemeRegistry = new SchemeRegistry();
    private PoolingClientConnectionManager cm;
    private boolean initialized = false;
    private void initHttpConfig() {
        schemeRegistry = new SchemeRegistry();
        if ( proxyEnable ) {
            schemeRegistry.register( new Scheme("http", 80, new SocksSchemeSocketFactory() ) );
        } else {
            schemeRegistry.register(new Scheme("http", 80, PlainSocketFactory.getSocketFactory()));
        }
        schemeRegistry.register( new Scheme("https", 443, SSLSocketFactory.getSocketFactory() ) );
        cm = new PoolingClientConnectionManager( schemeRegistry );
        cm.setMaxTotal( 10 );
        cm.setDefaultMaxPerRoute( 5 );
    }

    public DefaultHttpClient getHttpClient(){

        if ( !initialized ) {
            initHttpConfig();
            initialized = true;
        }

        HttpParams httpParams  = new BasicHttpParams();
        httpParams.setIntParameter( CoreConnectionPNames.CONNECTION_TIMEOUT, 8000 );
        httpParams.setIntParameter( CoreConnectionPNames.SO_TIMEOUT, 10000 );
        httpParams.setBooleanParameter( CoreProtocolPNames.USE_EXPECT_CONTINUE,  false );
        httpParams.setParameter( CoreProtocolPNames.PROTOCOL_VERSION, HttpVersion.HTTP_1_1 );
        httpParams.setParameter( ClientPNames.COOKIE_POLICY, CookiePolicy.IGNORE_COOKIES );

        if ( proxyEnable ) {
            ProxyConfig p = getProxyConfig();
            httpParams.setParameter(SocksSchemeSocketFactory.SOCKS_HOST, p.getAddress());
            httpParams.setParameter( SocksSchemeSocketFactory.SOCKS_PORT, p.getPort() );
        }

        return new DefaultHttpClient( cm, httpParams );
    }

    static class ProxyConfig {

        final String address;
        final Integer port;

        ProxyConfig(String address, Integer port) {
            this.address = address;
            this.port = port;
        }

        public String getAddress() {
            return address;
        }

        public Integer getPort() {
            return port;
        }

    }

    static class SocksSchemeSocketFactory implements SchemeSocketFactory {

        final static String SOCKS_HOST = "socks.host";
        final static String SOCKS_PORT = "socks.port";

        @Override
        public Socket connectSocket(final Socket socket,
                                    final InetSocketAddress remoteAddress,
                                    final InetSocketAddress localAddress,
                                    final HttpParams params)
                throws IOException {

            if ( remoteAddress == null ) {
                throw new IllegalArgumentException("Remote address may not be null");
            }
            if ( params == null ) {
                throw new IllegalArgumentException("HTTP parameters may not be null");
            }
            Socket sock;
            if ( socket != null ) {
                sock = socket;
            } else {
                sock = createSocket(params);
            }
            if ( localAddress != null ) {
                sock.setReuseAddress( HttpConnectionParams.getSoReuseaddr(params) );
                sock.bind( localAddress );
            }
            int timeout = HttpConnectionParams.getConnectionTimeout(params);
            try {
                sock.connect( remoteAddress, timeout );
            } catch (SocketTimeoutException ex) {
                throw new ConnectTimeoutException("Connect to " + remoteAddress.getHostName() + "/" + remoteAddress.getAddress() + " time out");
            }
            return sock;
        }

        @Override
        public Socket createSocket(final HttpParams params) throws IOException {
            if ( params == null ) {
                throw new IllegalArgumentException("Http parameters may not be null");
            }
            String proxyHost = (String) params.getParameter( SOCKS_HOST );
            Integer proxyPort = (Integer) params.getParameter( SOCKS_PORT );

            InetSocketAddress socksAddr = new InetSocketAddress(proxyHost, proxyPort);
            Proxy proxy = new Proxy( java.net.Proxy.Type.SOCKS, socksAddr );
            return new Socket(proxy);
        }

        @Override
        public boolean isSecure(final Socket sock) throws IllegalArgumentException {
            return false;
        }

    }

}
